package wfb.wfbdbimpl;

import wfb.definitions.*;

import java.sql.*;
import java.util.*;

import org.hibernate.*;
import org.hibernate.cfg.*;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.type.Type;

//import com.ibm.db2.jcc.a.c;
//import com.ibm.db2.jcc.c.co;
@SuppressWarnings("unchecked")
/** Schnittstelle, die von der Zugriffsschicht auf
 * die WorldFactbook-Datenbank implementiert werden muss.
 */
public class Impl implements WfbDbInterface {

	private SessionFactory sessionFactory;
	private Session session;

	HashMap<String, Land> länder = null; //obsolete
	HashMap<String, HashMap<String, Jahresdaten>> jahresdaten = null; //obsolete
	private HashMap<String, HashSet<String>> länderInOrga = null; //obsolete
	HashMap<String, HashSet<Stadt>> städte = null; //obsolete

	Properties prop = null;
	Connection con = null;
	Statement stmt = null;
	PreparedStatement pstmt = null;
	ResultSet rs = null;
	ResultSetMetaData rsmd = null;
	String query, url, userid, passwd, jdbcDriver, flagge, karte;
	ClassMetadata jahresdatenMetadata = null;
	Map<String, ClassMetadata> foo = null;

	private ArrayList<String> statAttrs = null;

	private HashMap<String, CountryData> countryData = new HashMap<String, CountryData>();

	/** Konstruktor */
	public Impl () {
	}

	/** Initialisierung der Zugriffsschicht. Das Property-Objekt
	 * enthaelt alle notwendigen Parameter.
	 * @param prop  Property-Objekt mit Attribut-Wert-Paaren, die
	 *              zur Initialisierung dienen (Login-Name, Passwort,...).
	 */
	@SuppressWarnings("unchecked")
	public void init(Properties prop) { // in dem Properties objekt sind die verbindungsdaten, 
		System.out.println("init()");
		this.prop=prop;

		url = prop.getProperty("jdbcURL");
		userid = prop.getProperty("user");
		passwd = prop.getProperty("pw");
		jdbcDriver = prop.getProperty("jdbcDriver");
		flagge = prop.getProperty("flagge");
		karte = prop.getProperty("karte");

		Properties prophibernate = new Properties();
		prophibernate.setProperty("hibernate.connection.driver_class", jdbcDriver);
		prophibernate.setProperty("hibernate.connection.url", url);
		prophibernate.setProperty("hibernate.connection.username", userid);
		prophibernate.setProperty("hibernate.connection.password", passwd);
		prophibernate.setProperty("hibernate.dialect", prop.getProperty("dialect"));
		prophibernate.setProperty("hibernate.cache.provider_class", prop.getProperty("provClass"));
		prophibernate.setProperty("hibernate.transaction.factory_class", prop.getProperty("transactionFac"));
		System.out.println("hib properties set up");

		try{
			sessionFactory = new Configuration()
			.addProperties(prophibernate)
			.addAnnotatedClass(Land.class)
			.addAnnotatedClass(Jahresdaten.class)
			.addAnnotatedClass(LandInOrga.class)
			.addAnnotatedClass(Stadt.class)
			.configure()
			.buildSessionFactory();
			System.out.println("sesFac set up");

			//to get all ojects obtained from db
			session = sessionFactory.openSession();
			session.beginTransaction();

			jahresdatenMetadata = sessionFactory.getClassMetadata(Jahresdaten.class);

		} catch(Exception ex) {
			ex.printStackTrace();
			System.exit(5);
		}  
		System.out.println("ende init()");
	}

	/** Diese Methode wird bei Beendigung der Anwendung aufgerufen. Hier koennen
	 * alle Ressourcen freigegeben werden (Verbindung usw.) 
	 */
	public void finish() {
		try {
			if (rs != null)
				rs.close();
			if (stmt != null)
				stmt.close();
			if (con != null)
				con.close();
		} catch (SQLException e1) {
			System.err.println(e1.getMessage());
		}
		if ( sessionFactory != null ) {
			sessionFactory.close();
		}
		System.out.println("Impl: finished.");
	}

	// -- Staedte- und Laenderdaten abfragen -----------------------------------
	/** Liefert eine Liste aller Laendernamen, die zu dem gegebenen Pattern
	 * (Suchausdruck) passen.
	 * @param namePattern  der Suchausdruck, der die zu liefernden Namen
	 *                     bestimmt; Wildcards entsprechen dem
	 *                     SQL-Standard; namePattern kann <code>null</code> sein!
	 * @return Liste von Laendernamen, die auf den Suchausdruck passen oder
	 *         im Falle, dass <code>namePattern==null</code>, die Liste aller
	 *         Laendernamen; die Listenobjekte sind vom Typ String
	 */
	public synchronized List<String> getCountries(String namePattern) {
		List<String> countries;
		if(namePattern!=null){
			countries = session.createQuery("select l.land from Land as l where l.land like '" + namePattern + "'").list();
		}else{
			countries = session.createQuery("select l.land from Land as l").list();
		}
		return countries;
	}

	/** Liefert ein Objekt mit Daten zu einem Land.
	 * @param countryName  Name des Landes, von dem die Daten geliefert
	 *                     werden sollen
	 * @return zeitunabhaengige Daten zu dem angegebenen Land
	 *         (im Wesentlichen Attribute der Landes-Tabelle) oder
	 *         <code>null</code> wenn kein Land mit diesem Namen existiert
	 */
	public synchronized CountryDataInterface getCountryData(String countryName) {
		Land country = (Land) session.createQuery("from Land as l where l.land = '"
				+ countryName + "'").list().get(0);
		country.setCapital((Stadt) session.createQuery("from Stadt as l where l.stadt = '" 
				+ country.getHauptstadt() + "' and l.land='"+countryName+"'").list().get(0));
		//country.setRefFlag(flagge+country.getFlagLink());
		//country.setRefMap(karte+country.getMapLink());
		country.setOrganizations((List<OrganizationDataInterface>) session.createQuery("from LandInOrga as o where o.land='"
				+countryName+"'").list());
		country.setCities((List<CityDataInterface>) session.createQuery("from Stadt as s where s.land='"+
				countryName+"'").list());
		return country;
	}

	/** Liefert eine Liste aller Organisationen.
	 * @return Liste der Abkuerzungen aller in der Datenbank enthaltenen
	 *         Organisationen; die Listenobjekte sind vom Typ String
	 */
	public synchronized List<String> getOrganizations() {
		List<String> orgas = (List<String>) session.createQuery("select distinct organisation from LandInOrga").list();
		return orgas;
	}

	/** Liefert die ausfuehrlichen Namen aller Attribute, die in der
	 * Statisitk-Tabelle verfuegbar sind (z.B. BIP, Einwohnerzahl, ...).
	 * @return Liste von Attributnamen der Statisitk-Tabelle (ohne Attribut Jahr)
	 */
	public synchronized List<String> getStatisticAttributes() {
		statAttrs = new ArrayList<String>(Arrays.asList((jahresdatenMetadata.getPropertyNames())));
		return statAttrs;
	}

	/** Liefert alle Jahre, zu denen Daten in der Statistik-Tabelle verfuegbar sind.
	 * @return Liste von Jahren zu denen es Eintraege in der Statisitk-Tabelle gibt
	 */
	public synchronized List<String> getStatisticYears() {
		List<Integer> jahre;
		jahre = (List<Integer>) session.createQuery("select distinct year from Jahresdaten").list();
		List<String> stjahre = new ArrayList<String>();
		for(Integer y : jahre){
			stjahre.add(y.toString());
		}
		//statYears=(ArrayList<String>) stjahre;
		return stjahre;
	}

	/** Liefert die statistischen Daten fuer alle angegebenen Laender und alle angegebenen
	 * Jahre. Es werden nur diejenigen Laender zurueckgeliefert, die den Bedingungen
	 * entsprechen, dass sie in allen Organisationen vertreten sind, die in inOrganization
	 * angegeben sind und in keiner Organisation vertreten sind, die in notInOrganization
	 * angegeben sind. Weiterhin sollen nur die statistischen Attribute zurueckgeliefert
	 * werden, die in attributes spezifiziert wurden.
	 *
	 * @param countries  die Laender, die als potentielle Kandidaten fuer die statistischen
	 *                   Daten in Frage kommen (beachte Auswahlkriterium Organisation!);
	 *                   wird hier <code>null</code> uebergeben, kommen alle Laender als
	 *                   Kandidaten in Frage
	 * @param years      die Jahrgaenge fuer die statistische Daten geliefert werden sollen;
	 *                   wird hier <code>null</code> angegeben, sollen alle verfuegbaren
	 *                   Jahrgaenge geliefert werden
	 * @param inOrganization  eine Liste von Organisationen; Laender fuer die statistische
	 *                        Daten geliefert werden sollen, muessen in allen hier gelisteten
	 *                        Organisationen vertreten sein (Filterkriterium fuer die
	 *                        angegeben Laender);
	 *                        wird hier <code>null</code> uebergeben, ist dieses
	 *                        Filterkriterium nicht wirksam
	 * @param notInOrganization  eine Liste von Organisationen; Laender fuer die statistische
	 *                           Daten geliefert werden sollen, duerfen in keiner hier
	 *                           gelisteten Organisationen vertreten sein (Filterkriterium
	 *                           fuer die angegeben Laender);
	 *                           wird hier <code>null</code> uebergeben, ist dieses
	 *                           Filterkriterium nicht wirksam
	 * @param attributes  eine Auswahl der statistischen Daten, die geliefert werden sollen;
	 *                    wird hier <code>null</code> uebergeben, sollen alle Attribute
	 *                    geliefert werden
	 * @return Datenobjekt, das alle gewuenschten statistischen Informationen fuer alle
	 *         Laender, die den Filterkriterien entsprechen und alle angegeben Jahrgaenge
	 *         enthaelt 
	 */
	public synchronized StatData getStatisticData(String[] countries, String[] years,
			String[] inOrganization,
			String[] notInOrganization,
			String[] attributes) {

		String query = "";
		if ((countries != null) && (countries.length == 0)) countries = null;
		if ((years != null) && (years.length == 0)) years = null;
		if ((inOrganization != null) && (inOrganization.length == 0)) inOrganization = null;
		if ((notInOrganization != null) && (notInOrganization.length == 0)) notInOrganization = null;
		if ((attributes != null) && (attributes.length == 0)) attributes = null;

		if (attributes != null){
			query += "select land, year, ";
			for( String a : attributes){
				query += a;
				if(a != attributes[attributes.length-1]) query += ", ";
			}
			query += " ";
		}
		query += "from Jahresdaten";

		//get fitting countrynames:
		if(inOrganization!=null||notInOrganization!=null){
			String cquery = "Select distinct land from LandInOrga where ";
			if(countries != null){
				cquery += "land in (";
				for (String s : countries) {
					cquery += "'"+s;
					if (s == countries[countries.length - 1]) {
						cquery += "') ";
					} else {
						cquery += "', ";
					}
				}
				if(inOrganization!=null || notInOrganization!=null) cquery+= "and ";
			}
			if (inOrganization != null) {
				cquery += "organisation in (";
				for (String s : inOrganization) {
					cquery += "'"+s;
					if (s == inOrganization[inOrganization.length - 1]) {
						cquery += "') ";
					} else {
						cquery += "', ";
					}
				}
				if(notInOrganization!=null) cquery+= "and ";
			}
			if (notInOrganization != null) {
				cquery += "organisation not in (";
				for (String s : notInOrganization) {
					cquery += "'"+s;
					if (s == notInOrganization[notInOrganization.length - 1]) {
						cquery += "') ";
					} else {
						cquery += "', ";
					}
				}
			}
			//System.out.println(cquery);
			List<String> selcount = session.createQuery(cquery).list();
			countries = selcount.toArray(countries);
		}
		if(countries!=null || years!=null) query+=" where ";

		if (countries != null) {
			query += "land in (";
			for (String s : countries) {
				query += "'"+s+"'";
				if (s == countries[countries.length - 1]) {
					query += ") ";
				} else {
					query += ", ";
				}
			}
			if(years!= null) query+="and ";
		}

		if (years!= null) {
			query += "year in (";
			for (String s : years) {
				query += s;
				if (s == years[years.length - 1]) {
					query += ") ";
				} else {
					query += ", ";
				}
			}
		}

		List<Jahresdaten> ret = null;
		List<Object[]> ret2 = null;
		//System.out.println(query);
		if(attributes == null){
			ret = session.createQuery(query).list();
		}else{
			ret2 = session.createQuery(query).list();
		}
		int it = (ret==null)?ret2.size():ret.size();

		String[]attrSeq = (attributes==null)?jahresdatenMetadata.getPropertyNames():attributes;
		StatData retStatData = new StatData(attrSeq);
		//List<String> retYears = (years == null) ? statYears : Arrays.asList(years);
		String [] retattrs;

		for( int i = 0; i <it ; i++){
			String aktcount = (ret==null)?ret2.get(i)[0].toString():ret.get(i).getLand();
			String aktjahr = (ret==null)?ret2.get(i)[1].toString():ret.get(i).getYear().toString();
			retattrs = new String[attrSeq.length];
			for (int j = 0; j < attrSeq.length; j++) {
				try{
					if(attrSeq[j].equalsIgnoreCase("year")){
						retattrs[j] = (ret==null)?ret2.get(i)[j+2].toString():ret.get(i).getYear().toString();
					}else if(attrSeq[j].equalsIgnoreCase("bip")){
						retattrs[j] = (ret==null)?ret2.get(i)[j+2].toString():ret.get(i).getBip().toString();
					}else if(attrSeq[j].equalsIgnoreCase("einwohner")){
						retattrs[j] = (ret==null)?ret2.get(i)[j+2].toString():ret.get(i).getEinwohner().toString();
					}else if(attrSeq[j].equalsIgnoreCase("lebenserwartungm")){
						retattrs[j] = (ret==null)?ret2.get(i)[j+2].toString():ret.get(i).getLebenserwartungm().toString();
					}else if(attrSeq[j].equalsIgnoreCase("lebenserwartungw")){
						retattrs[j] = (ret==null)?ret2.get(i)[j+2].toString():ret.get(i).getLebenserwartungw().toString();
					}
				}catch (NullPointerException e){}
			}
			try {
				retStatData.addData(aktcount, aktjahr, retattrs);
			} catch (WfbDbException e) {
				System.err.println(e.getMessage());
			}
		}
		return retStatData;
	}

	// -- Update-Operation -----------------------------------------------------
	/** Ersetzt einen Kommentar zu einem Land durch einen neuen Kommentar.
	 * @param country  das Land fuer das der Kommentar ersetzt werden soll
	 * @param comment der neue Kommentar
	 * @exception WfbDbException  wenn waehrend der Operation ein Fehler auftrat
	 */
	public synchronized void updateCountryComment(String country, String comment) throws WfbDbException {

//		CountryData cData = (CountryData) session.createQuery("from Land as l where l.land='"+
//							country+"'").list().get(0);
//		if ((cData = countryData.get(country)) != null){
//			cData.setComment(comment);
//			session.update(cData);
//		}
		this.executeSqlQuery("update dbprak13.Country set comm='" + comment + 
				"' where short='" + country + "'");
	}

	// -- Direkter Datenbankzugriff --------------------------------------------
	/** Fuehrt die gegebene Operation auf der Datenbank aus und liefert
	 * das Ergebnis in einem SQLResultInterface-Objekt aufbereitet zurueck.<br>
	 * Achtung: es kann sich bei der Operation sowohl um eine Anfrage als auch
	 * TODO     um eine Aenderungsoperation (insert, drop, ...) handeln. Dieses
	 *          muss beruecksichtigt werden (Hinweis: Statement.execute()). Je
	 *          nach Art der Operation wird das Anfrageergebnis oder numerische
	 *          Rueckgabewert der Aenderungsoperation von der Methode zurueckgegeben.
	 * @param query  die auszufuehrende Operation
	 * @return das Ergebnis der Operation als SQLResultInterface-Objekt; entweder
	 *         das Anfrageergebnis oder der numerische Wert bei einer Update-Operation
	 * @exception WfbDbException  wenn waehrend der Ausfuehrung der Anfrage
	 *                            ein Fehler auftrat (z.B. java.sql.SQLException)
	 */
	public synchronized SQLResultInterface executeSqlQuery(String query) throws WfbDbException {
		class SQLResImpl implements SQLResultInterface{
			String[] attrNames = null;
			String[][] rows = null;
			int row = -1;
			SQLResImpl(String[] attrNames, String[][] rows) {
				this.attrNames=attrNames;
				this.rows = rows;
			}
			public List<String> getHeader() throws WfbDbException {
				return Arrays.asList(attrNames);
			}
			public String[] getNextRow() throws WfbDbException {
				return (++row < rows.length) ? rows[row] : null;
			}
		}

		// definiere 3 verschiedene Ergebnisse (abhaengig von Laenge der Anfrage)
		if ((query == null) || (query.length()==0))
			throw new WfbDbException("Keine Anfrage uebergeben!");

		try {
			con = DriverManager.getConnection(url, userid, passwd);

			stmt = con.createStatement();
			if(query.contains("select")){
				rs = stmt.executeQuery(query);
			}else{
				stmt.execute(query);
			    System.out.println(stmt.getUpdateCount() + " Zeilen betroffen.");
			}
			if(rs!=null){
			rsmd = rs.getMetaData();

			int cc = rsmd.getColumnCount();
			String[] sqlHead = new String[cc];
			ArrayList<String[]> tmpArray = new ArrayList<String []>(); 

			for (int i = 0; i < cc; i++) {	// get column names
				sqlHead[i] = rsmd.getColumnName(i+1);
				System.out.println(sqlHead[i]);
			}

			while (rs.next()) { 			// save rows in tmpArray
				String[] tmp = new String[cc];
				for (int i = 0; i < cc; i++) {
					System.out.println(rs.getString(i+1));
					tmp[i] = rs.getString(i+1);				
				}
				tmpArray.add(tmp);
			}
			rs.close();

			System.out.println("tmpArray.size(): " + tmpArray.size());
			String[][] sqlRows = new String[tmpArray.size()][cc];
			System.out.println("nach sqlRows");

			int row = 0;					// transfer dyn to static array
			for (String[] rowString: tmpArray) {
				int col = 0;
				for (String sub: rowString) {
					sqlRows[row][col] = sub;
					col++;
				}
				row++;
			}
			// TODO anzahl geänderter datensätze?
			return new SQLResImpl(sqlHead, sqlRows);}
		} catch (SQLException e) {
			System.err.println("Fehler beim Ausfuehren " +
					"einer Datenbankoperation aufgetreten:\n" + e.getMessage());
			e.printStackTrace();
		}
		return null;
	}

	// -- Direkter Datenbankzugriff per HQL ------------------------------------
	public synchronized SQLResultInterface executeHqlQuery(String query, boolean deref) throws WfbDbException {
		System.out.println("executeHqlQuery()");
		class SQLResImpl implements SQLResultInterface{
			String[] attrNames = null;
			String[][] rows = null;
			int row = -1;
			SQLResImpl(String[] attrNames, String[][] rows) {
				this.attrNames=attrNames;
				this.rows = rows;
			}
			public List<String> getHeader() throws WfbDbException {
				return Arrays.asList(attrNames);
			}
			public String[] getNextRow() throws WfbDbException {
				return (++row < rows.length) ? rows[row] : null;
			}
		}

		try {
			session = sessionFactory.openSession();
			session.beginTransaction();
			Query randomQuery = session.createQuery(query);
			List randomList = randomQuery.list();			
			ClassMetadata meta = null;
			String[] sqlHead = null;
			String[][] sqlRows = null;
			ArrayList<String[]> data = new ArrayList<String[]>();
			int sqlHeadLength = 0;

			if (!randomList.isEmpty()) {
				Type singleType = randomQuery.getReturnTypes()[0];
				meta = sessionFactory.getClassMetadata(singleType.getName());
				if (singleType.isAssociationType()) { // z.B. Land
					ArrayList<String> rowNames = new ArrayList<String>();

					for (String s: meta.getPropertyNames()) {
						System.out.println(s);
						rowNames.add(s);
						sqlHeadLength++;
					}
					sqlHead = rowNames.toArray(new String[sqlHeadLength]);

					Type[] propertyTypes = meta.getPropertyTypes();
					for (Object o: randomList) {
						String[] row = new String[sqlHeadLength];
						Object[] values = meta.getPropertyValues(o, EntityMode.POJO);
						int count = 0;
						for (Object entry: values) {
							if (entry == null) {
								row[count] = "";
							} else if (propertyTypes[count].toString().equals("String")) {
								row[count] = (String) entry;
							} else {
								row[count] = entry.toString();
							}
							count++;
						}
						data.add(row);
					}
				} else if (randomList.get(0).getClass().getSimpleName().equals("String") ||
						randomList.get(0).getClass().getSimpleName().equals("Integer") ||
						randomList.get(0).getClass().getSimpleName().equals("Double")) {
					for (Object o: new ArrayList<Object>(randomList)) {
						if (o == null) {
							data.add(new String[] {""});
						} else {
						data.add(new String[] {o.toString()});
						}
					}
					sqlHeadLength = 1;
					Scanner scanner = new Scanner(randomQuery.getQueryString());
					while (scanner.hasNext()) {
						String tmp = scanner.next();
						if (!tmp.equals("select")) {
							if (tmp.equals("from")) {
								break;
							}
							sqlHead = new String[] {tmp};
						}
					}
				} else {
					Scanner scanner = new Scanner(randomQuery.getQueryString());
					ArrayList<String> colNames =  new ArrayList<String>();
					while (scanner.hasNext()) {
						String tmp = scanner.next();
						if (!tmp.equals("select") && !tmp.equals("distinct")) {
							if (tmp.equals("from")) {
								break;
							}
							colNames.add(tmp);
						}
					}
					sqlHeadLength = colNames.size();
					sqlHead = colNames.toArray(new String[sqlHeadLength]);
					for (Object[] o: (List<Object[]>) randomList) {
						int count = 0;
						String[] row = new String[sqlHeadLength];
						for (Object entry: o) {
							if (entry == null) {
								row[count] = "";
							} else {
								row[count] = entry.toString();
							}
							count++;
						}
						data.add(row);
					}
				}
			}

			sqlRows = data.toArray(new String[sqlHeadLength][data.size()]);

			return new SQLResImpl(sqlHead, sqlRows);
		} catch (Exception e) {
			e.printStackTrace();
			System.err.println(e.getMessage());
		}
		return null;
	}
}
